package ai.tree;

import io.Logger;

import java.util.ArrayList;

import ai.AI;

import structure.Board;
import structure.Move;
import utils.Param;

public class Node {

	public static int LEVEL_MAX;
	private static AI ai = null;

	private int level;
	private ArrayList<Edge> moves;
	private float nodeAB;

	// 0 if Node is not terminal or if Evaluation Function hasn't been called
	// yet.
	// +1 if it is terminal and already has been computed for white.
	// -1 if it is terminal and already has been computed for black.
	byte nodeABLastComputedWith;
	boolean isTerminal;
	
	
	public static void registerAI(AI ai){
		Node.ai = ai;
	}
	
	

	public Node(int level) {
		this.level = level;
	}

	public ArrayList<Evaluation> getBestMoves() {
		if (!Param.TIME_OPTIMIZATION_ENABLED)
			reshuffle(0, moves.size());

		ArrayList<Evaluation> answer = new ArrayList<Evaluation>();

		if (moves.size() > 0) {
			for (int i = 0; i < moves.size(); i++)
				answer.add(new Evaluation(moves.get(i).move, moves.get(i).node
						.getNodeAB()));
		} else {
			Logger.logln(Node.class,"ERROR:: Move selection unavailable because Memory depth exceeded.");
			System.exit(0);
		}
		return answer;
	}

	public ArrayList<Edge> getBestNodes() {
		if (Param.TIME_OPTIMIZATION_ENABLED)
			return moves;
		else {
			reshuffle(0, moves.size());
			return moves;
		}
	}

	public float initSearch(Board board, byte color, double initTime) {

		if (moves.size() == 0) {
			ArrayList<Move> moveList = board
					.getValidMoves((byte) (isMax(0) ? color : color * -1));
			for (int i = 0; i < moveList.size(); i++)
				moves.add(new Edge(moveList.get(i), new Node(0)));
		}

		// Re-init fNodeAB
		nodeAB = isMax(0) ? Float.MIN_VALUE : Float.MAX_VALUE;

		/*
		 * For all the possible moves from this node we look for the max
		 * (respectively min)
		 */
		isTerminal = true;
		double duration;
		int imove;

		for (imove = 0; imove < moves.size(); imove++) {
			nodeAB = minMax(
					(moves.get(imove).node).getPathScore(board,
							moves.get(imove).move, nodeAB, 1, color, initTime),
					nodeAB, 0);
			isTerminal = (isTerminal && moves.get(imove).node.isTerminal());

			/*
			 * If time is over and if it's not the first time that we entered
			 * the search function (level max is > 2) then we should return...
			 */
			duration = ((double) System.nanoTime()) - initTime;
			if (Param.TIME_OPTIMIZATION_ENABLED && duration >= Param.TIME_LIMIT
					&& LEVEL_MAX > Param.INIT_DEPTH_SEARCH) {
				reshuffle(0, imove);
				Logger.logln(Node.class," Time out, leaving level :: 0 with "
						+ imove + " moves analysed.");
				return nodeAB;
			}

		}
		reshuffle(0, moves.size());
		return nodeAB;
	}

	private boolean isTerminal() {
		return isTerminal;
	}

	protected float getPathScore(Board board, Move move, float fatherAlpha,
			int level, byte Color, double initTime) {

		board.doMove(move);

		if (moves.size() == 0) {
			ArrayList<Move> moveList = board
					.getValidMoves((byte) (isMax(level) ? Color : (Color * -1)));
			for (int i = 0; i < moveList.size(); i++)
				moves.add(new Edge(moveList.get(i), new Node(level)));
		}

		// If we reach the level we want to check we just have to return the
		// board evaluation
		if (board.isOver()
				|| ((level >= Param.MEM_LEVEL_MAX) && moves.size() > Param.N_MOVES_LIMIT)) {
			isTerminal = board.isOver();
			if (nodeABLastComputedWith != Color) {
				nodeABLastComputedWith = Color;
				nodeAB = ai.fe(board, Color);
			}
			return nodeAB;
		}

		// Init with high/low values so the min/max will work
		float alpha, beta;
		float oldNodeAB;

		nodeAB = isMax(level) ? Float.MIN_VALUE : Float.MAX_VALUE;

		/*
		 * For all the possible moves from this node we look for the max
		 * (respectively min)
		 */
		isTerminal = true;
		int imove = 0;
		double duration;
		for (imove = 0; imove < moves.size(); imove++) {
			oldNodeAB = nodeAB;
			nodeAB = minMax(
					(moves.get(imove).node).getPathScore(board,
							moves.get(imove).move, nodeAB, (level + 1), Color,
							initTime), nodeAB, level);
			isTerminal = (isTerminal && moves.get(imove).node.isTerminal());
			if (nodeAB != oldNodeAB) {
				alpha = isMax(level) ? nodeAB : fatherAlpha;
				beta = isMax(level) ? fatherAlpha : nodeAB;

				// If alpha >= beta then there is no need to check this node
				// anymore...
				if (alpha > beta && Param.DO_PRUNING) {
					reshuffle(level, moves.size());
					return nodeAB;
				}
			}

			/*
			 * If time is over, and it is not the maxlevel-1 situation and if
			 * it's not the first time that we entered the search function
			 * (level max is > 2) then we should return...
			 */
			duration = ((double) System.nanoTime()) - initTime;
			if (Param.TIME_OPTIMIZATION_ENABLED
					&& duration >= (Param.TIME_LIMIT)
					&& (level + 1) < LEVEL_MAX
					&& LEVEL_MAX > Param.INIT_DEPTH_SEARCH) {
				reshuffle(level, imove);
				Logger.logln(Node.class," Time out, leaving level :: " + level
						+ " with " + imove + " moves analysed.");
				return nodeAB;
			}

		}
		reshuffle(level, moves.size());
		return nodeAB;
	}

	private void reshuffle(int fLevel, int size) {
		if (level > Param.MEM_LEVEL_MAX)
			moves.clear();
		else
			quickSort(size);

	}

	public Node getNodeForMove(Move move) {
		for (int i = 0; i < moves.size(); i++) {
			if (moves.get(i).move.equals(move))
				return moves.get(i).node;
		}

		if (moves.size() != 0)
			Logger.logln(Node.class,"WARNING:: A Node has been asked for a move that was not considered. move="
							+ move);
		else
			Logger.logln(Node.class,"WARNING:: A Node has been asked for a move but it had exceeded memorization depth limit. ");
		return new Node(0);
	}

	void printTree() {
		for (int i = 0; i < moves.size(); i++)
			moves.get(i).node.printNodes(0, moves.get(i).move);
	}

	private void printNodes(int fLevel, Move move) {
		if (nodeAB < Float.MAX_VALUE && nodeAB > Float.MIN_VALUE) {
			for (int i = 0; i < fLevel; i++)
				Logger.log(Node.class,"-");
			Logger.log(Node.class,move + " : " + nodeAB
					+ (isTerminal ? " : Terminal" : " : No Terminal\n"));

			for (int i = 0; i < moves.size(); i++)
				moves.get(i).node.printNodes(fLevel + 1, moves.get(i).move);
		}
		Logger.flush();
	}

	public float getNodeAB() {
		return nodeAB;
	}

	public float minMax(float a, float b, int fLevel) {
		return isMax(level) ? Math.max(a, b) : Math.min(a, b);
	}

	boolean isMax(int fLevel) {
		return (fLevel % 2) == 0;
	}

	private void quickSort(int size) {
		int[] beg = new int[Param.QS_MAX_LEVELS];
		int[] end = new int[Param.QS_MAX_LEVELS];
		int i = 0, L, R;
		Edge piv = moves.get(0);
		beg[0] = 0;
		end[0] = size;
		while (i >= 0) {
			L = beg[i];
			R = end[i] - 1;
			if (L < R) {
				piv = moves.get(L);
				while (L < R) {
					while (moves.get(R).node.getNodeAB() <= piv.node
							.getNodeAB() && L < R)
						R--;
					if (L < R)
						moves.set(L++, moves.get(R));
					while (moves.get(L).node.getNodeAB() >= piv.node
							.getNodeAB() && L < R)
						L++;
					if (L < R)
						moves.set(R--, moves.get(L));
				}
				moves.set(L, piv);
				beg[i + 1] = L + 1;
				end[i + 1] = end[i];
				end[i++] = L;
			} else {
				i--;
			}
		}
	}

}
